JWT Access Token Validation – OAuth Architecture Guidance
Background
Previously we drilled into API Coding Key Points for the initial code sample. Next we will describe some details and technical choices when validating JWTs in APIs.
Common Requirements
When designing token validation in APIs there are a few factors to consider:
Factor | Description |
---|---|
Simple Code | Validating a JWT should not require a great amount of code or complicate your API |
Understanding | You should understand how the validation works, so that you can ensure it is doing the right things |
Extensible | JWT validation should fit with a wider plan to ensure that APIs have what they need to authorize requests |
Security Capabilities | Some libraries have better capabilities than others, such as the ability to use financial-grade algorithms |
Useful Errors | You must ensure that API clients receive useful error responses when access tokens fail validation |
Libraries v Frameworks
Some technology stacks provide a ‘Resource Server Framework’ that operates like a black box and requires very little code. This is fine when all you need is the default behaviour, but this blog uses a library approach for better control over the above behaviour.
JOSE Libraries
This blog will provide APIs developed in the following languages and in each case will use a JOSE library, so that the most leading edge security options are available, in case ever needed in future:
JOSE libraries support a number of OAuth related security specifications:
JWT Validation Code
The JWT validation code in our initial API required very little code, and we will explain the key behaviour in the following sections:
Viewing JWT Access Tokens
JWT access tokens are issued by the Authorization Server and consist of three parts. Cognito uses an asymmetric private key to create the digital signature:
Here is an example AWS Cognito JWT access token:
eyJraWQiOiIyV01TWGcwekEydVFlTjE0ZWlma0o5Nk5TTURpUmdtSXNGcE9yNHNJVWRvPSIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiJhNmI0MDRiMS05OGFmLTQxYTItOGU3Zi1lNDA2MWRjMGJmODYiLCJpc3MiOiJodHRwczpcL1wvY29nbml0by1pZHAuZXUtd2VzdC0yLmFtYXpvbmF3cy5jb21cL2V1LXdlc3QtMl9xcUpnVmV1VG4iLCJ2ZXJzaW9uIjoyLCJjbGllbnRfaWQiOiI2dGcwcWdsZGRwdnFoNzRrM2piZjFtbWo2NCIsIm9yaWdpbl9qdGkiOiI0NDNmNGMzNS1iNmRiLTQzYzktODgzYS0wMThmOWM5NzMzOGMiLCJldmVudF9pZCI6ImMyMGViMGI1LWJhODQtNDMzNC04M2NhLWI3NWExMGQ1ZmY0NyIsInRva2VuX3VzZSI6ImFjY2VzcyIsInNjb3BlIjoib3BlbmlkIHByb2ZpbGUiLCJhdXRoX3RpbWUiOjE2MzQ5NzgwNDQsImV4cCI6MTYzNDk4MTY0NCwiaWF0IjoxNjM0OTc4MDQ0LCJqdGkiOiI3Zjk0YzJhNC0zNWE3LTQ2NGItODFkMC05MDQ4YmUzODBhODkiLCJ1c2VybmFtZSI6ImE2YjQwNGIxLTk4YWYtNDFhMi04ZTdmLWU0MDYxZGMwYmY4NiJ9.CM5j3AXOGNL77AjW1-QImb6uwR8JE6ZojWOfKI-nZJhkCsDlSmG2qMpq6Ntkm-Pve6zA9TkWbCWSA1MHKwgQPMXobz5UDQSJSGwiEIa4L9Q6eCGIEDs5153DkXRD4KLYu-SGLOgSurzRuc-EUINDA7zyErNDKGbaFf8qPV5QuMTCQGO-h1SkvLU85yc8Xp6Q8MYv9ydf1oWukjCJdDSzlUdjP6Vsb3V5xKaTBWFvHpwoo5cwyD51Pu8Lsu7p7B-vQAfzXjfgPjnc5EQY_fNYZoh9MaB6b3EnGgZz0oY9gCZHhlr_cRxgZlR_-J9KeUIYcW5Mna-J5GYFe6eRcEePxw
We can paste this into an Online JWT Viewer, to view the details, and note the Key Identifier (kid) field in the JWT header:
API Validation Steps
APIs must validate JWT access tokens on every request, which is designed to be a fast and scalable operation. The API must provide correct inputs to the security library in order for this to be done correctly:
Check | Description |
---|---|
Algorithm | The API specifies one or more algorithms that can be used, and AWS Cognito tokens use the mainstream RS256 option |
Issuer | The API expects the issuer in the token to be the value from OpenID Connect metadata |
Audience | The audience represents a set of related APIs, and the API must specify a value such as api.mycompany.com |
Time | Access tokens should be short lived, such as 30 minutes, and the API must check the token is valid for use and not expired |
Public Key | The API provides the token signing public key to the library, and the most common way to do this is via a trusted download URL |
API OAuth Configuration
The initial API reflected the above settings in its configuration file. Note that AWS Cognito does not support an audience claim for access tokens so this was left blank.
Failed Token Validation
When any token validation checks fail, the API should return an error response with a 401 status code. I also like to return a clear payload as follows:
"code": "unauthorized",
"message": "Missing, invalid or expired access token"
In production systems this type of error is expected to occur frequently, since UI clients use short lived access tokens. When a client receives this response, a token refresh operation will be performed.
JWT Algorithms
An attacker could send an untrusted JWT with ‘alg: none’ in the JWT header, to bypass cryptographic security, but this cannot happen if you provide only secure algorithms to the security library.
Token Signing Public Keys
The API provides the security library with a trusted JWKS Endpoint and that for my AWS Cognito endpoint is at the below address:
This endpoint returns multiple public keys in a JSON Web Keyset and each of these has a key identifier. The entry whose kid value matches that in the access token’s JWT header is used to verify the token:
JWT Signature Verification
The above JWT Viewer website allows us to manually paste in a JSON Web Key. If we paste in a value from the JWKS endpoint whose kid does not match that in the JWT header we get a verification failure:
This result will also be returned if an attacker manually creates a JWT and sends it to the API, or if the JWT is tampered with and its contents altered.
JSON Web Keyset Caching
Any good JWT library will also cache the JSON Web Keyset details, with the following behaviour on subsequent requests:
Check | Description |
---|---|
Same kid | The cached JSON Web Key is used, to prevent the need for further calls to the Authorization Server’s JWKS endpoint |
New kid | A new call to the JWKS Endpoint is used, to get updated JSON Web Keys that include the new key identifier |
Token Signing Key Rotation
The Authorization Server will rotate its cryptographic keys occasionally, which will result in a new private key being used to sign JWTs and new public keys being made available for verification.
For a while the JWKS Endpoint will then return both old and new keys, until the old one is retired. A good API security library will cache JWKs correctly, so that the API automatically and reliably copes with renewal.
API Authorization
Once the JWT processing is done, the claims in the JWT payload can be trusted by the API and used to authorize requests for API data. We will discuss use of claims in further detail in this blog’s Authorization Design.
Where Are We?
We covered the key behaviour and design choices when validating JWTs in APIs. Next we will make some recommendations on getting started with your own Authorization Server.